/* * Copyright 2006-2009 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.batch.core.configuration.xml; import java.util.List; import org.w3c.dom.Element; import org.springframework.batch.core.listener.StepListenerMetaData; import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; /** * Internal parser for the <chunk/> element inside a step. * * @author Thomas Risberg * @since 2.0 */ public class ChunkElementParser { private static final String REF_ATTR = "ref"; private static final String MERGE_ATTR = "merge"; private static final String COMMIT_INTERVAL_ATTR = "commit-interval"; private static final String CHUNK_COMPLETION_POLICY_ATTR = "chunk-completion-policy"; private static final String BEAN_ELE = "bean"; private static final String REF_ELE = "ref"; private static final String ITEM_READER_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemReaderAdapter"; private static final String ITEM_PROCESSOR_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemProcessorAdapter"; private static final String ITEM_WRITER_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemWriterAdapter"; private static final StepListenerParser stepListenerParser = new StepListenerParser( StepListenerMetaData.itemListenerMetaData()); /** * @param element the element to parse * @param parserContext the context to use */ protected void parse(Element element, AbstractBeanDefinition bd, ParserContext parserContext, boolean underspecified) { MutablePropertyValues propertyValues = bd.getPropertyValues(); propertyValues.addPropertyValue("hasChunkElement", Boolean.TRUE); handleItemHandler(bd, "reader", "itemReader", ITEM_READER_ADAPTER_CLASS, true, element, parserContext, propertyValues, underspecified); handleItemHandler(bd, "processor", "itemProcessor", ITEM_PROCESSOR_ADAPTER_CLASS, false, element, parserContext, propertyValues, underspecified); handleItemHandler(bd, "writer", "itemWriter", ITEM_WRITER_ADAPTER_CLASS, true, element, parserContext, propertyValues, underspecified); String commitInterval = element.getAttribute(COMMIT_INTERVAL_ATTR); if (StringUtils.hasText(commitInterval)) { if (commitInterval.startsWith("#")) { // It's a late binding expression, so we need step scope... BeanDefinitionBuilder completionPolicy = BeanDefinitionBuilder .genericBeanDefinition(SimpleCompletionPolicy.class); completionPolicy.addConstructorArgValue(commitInterval); completionPolicy.setScope("step"); propertyValues.addPropertyValue("chunkCompletionPolicy", completionPolicy.getBeanDefinition()); } else { propertyValues.addPropertyValue("commitInterval", commitInterval); } } String completionPolicyRef = element.getAttribute(CHUNK_COMPLETION_POLICY_ATTR); if (StringUtils.hasText(completionPolicyRef)) { RuntimeBeanReference completionPolicy = new RuntimeBeanReference(completionPolicyRef); propertyValues.addPropertyValue("chunkCompletionPolicy", completionPolicy); } if (!underspecified && propertyValues.contains("commitInterval") == propertyValues.contains("chunkCompletionPolicy")) { if (propertyValues.contains("commitInterval")) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> element must contain either '" + COMMIT_INTERVAL_ATTR + "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "', but not both.", element); } else { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> element must contain either '" + COMMIT_INTERVAL_ATTR + "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "'.", element); } } String skipLimit = element.getAttribute("skip-limit"); ManagedMap<TypedStringValue, Boolean> skippableExceptions = new ExceptionElementParser().parse(element, parserContext, "skippable-exception-classes"); if (StringUtils.hasText(skipLimit)) { if (skippableExceptions == null) { skippableExceptions = new ManagedMap<TypedStringValue, Boolean>(); skippableExceptions.setMergeEnabled(true); } propertyValues.addPropertyValue("skipLimit", skipLimit); } if (skippableExceptions != null) { List<Element> exceptionClassElements = DomUtils.getChildElementsByTagName(element, "skippable-exception-classes"); if(!CollectionUtils.isEmpty(exceptionClassElements)) { skippableExceptions.setMergeEnabled(exceptionClassElements.get(0).hasAttribute(MERGE_ATTR) && Boolean.valueOf(exceptionClassElements.get(0).getAttribute(MERGE_ATTR))); } // Even if there is no retryLimit, we can still accept exception // classes for an abstract parent bean definition propertyValues.addPropertyValue("skippableExceptionClasses", skippableExceptions); } handleItemHandler(bd, "skip-policy", "skipPolicy", null, false, element, parserContext, propertyValues, underspecified); String retryLimit = element.getAttribute("retry-limit"); ManagedMap<TypedStringValue, Boolean> retryableExceptions = new ExceptionElementParser().parse(element, parserContext, "retryable-exception-classes"); if (StringUtils.hasText(retryLimit)) { if (retryableExceptions == null) { retryableExceptions = new ManagedMap<TypedStringValue, Boolean>(); retryableExceptions.setMergeEnabled(true); } propertyValues.addPropertyValue("retryLimit", retryLimit); } if (retryableExceptions != null) { List<Element> exceptionClassElements = DomUtils.getChildElementsByTagName(element, "retryable-exception-classes"); if(!CollectionUtils.isEmpty(exceptionClassElements)) { retryableExceptions.setMergeEnabled(exceptionClassElements.get(0).hasAttribute(MERGE_ATTR) && Boolean.valueOf(exceptionClassElements.get(0).getAttribute(MERGE_ATTR))); } // Even if there is no retryLimit, we can still accept exception // classes for an abstract parent bean definition propertyValues.addPropertyValue("retryableExceptionClasses", retryableExceptions); } handleItemHandler(bd, "retry-policy", "retryPolicy", null, false, element, parserContext, propertyValues, underspecified); String cacheCapacity = element.getAttribute("cache-capacity"); if (StringUtils.hasText(cacheCapacity)) { propertyValues.addPropertyValue("cacheCapacity", cacheCapacity); } String isReaderTransactionalQueue = element.getAttribute("reader-transactional-queue"); if (StringUtils.hasText(isReaderTransactionalQueue)) { propertyValues.addPropertyValue("isReaderTransactionalQueue", isReaderTransactionalQueue); } String isProcessorTransactional = element.getAttribute("processor-transactional"); if (StringUtils.hasText(isProcessorTransactional)) { propertyValues.addPropertyValue("processorTransactional", isProcessorTransactional); } handleRetryListenersElement(element, propertyValues, parserContext, bd); handleStreamsElement(element, propertyValues, parserContext); stepListenerParser.handleListenersElement(element, bd, parserContext); } /** * Handle the ItemReader, ItemProcessor, and ItemWriter attributes/elements. */ private void handleItemHandler(AbstractBeanDefinition enclosing, String handlerName, String propertyName, String adapterClassName, boolean required, Element element, ParserContext parserContext, MutablePropertyValues propertyValues, boolean underspecified) { String refName = element.getAttribute(handlerName); List<Element> children = DomUtils.getChildElementsByTagName(element, handlerName); if (children.size() == 1) { if (StringUtils.hasText(refName)) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> element may not have both a '" + handlerName + "' attribute and a <" + handlerName + "/> element.", element); } handleItemHandlerElement(enclosing, propertyName, adapterClassName, propertyValues, children.get(0), parserContext); } else if (children.size() > 1) { parserContext.getReaderContext().error( "The <" + handlerName + "/> element may not appear more than once in a single <" + element.getNodeName() + "/>.", element); } else if (StringUtils.hasText(refName)) { propertyValues.addPropertyValue(propertyName, new RuntimeBeanReference(refName)); } else if (required && !underspecified) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> element has neither a '" + handlerName + "' attribute nor a <" + handlerName + "/> element.", element); } } /** * Handle the <reader/>, <processor/>, or <writer/> that * is defined within the item handler. */ private void handleItemHandlerElement(AbstractBeanDefinition enclosing, String propertyName, String adapterClassName, MutablePropertyValues propertyValues, Element element, ParserContext parserContext) { List<Element> beanElements = DomUtils.getChildElementsByTagName(element, BEAN_ELE); List<Element> refElements = DomUtils.getChildElementsByTagName(element, REF_ELE); if (beanElements.size() + refElements.size() != 1) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> must have exactly one of either a <" + BEAN_ELE + "/> element or a <" + REF_ELE + "/> element.", element); } else if (beanElements.size() == 1) { Element beanElement = beanElements.get(0); BeanDefinitionHolder beanDefinitionHolder = parserContext.getDelegate().parseBeanDefinitionElement( beanElement, enclosing); parserContext.getDelegate().decorateBeanDefinitionIfRequired(beanElement, beanDefinitionHolder); propertyValues.addPropertyValue(propertyName, beanDefinitionHolder); } else if (refElements.size() == 1) { propertyValues.addPropertyValue(propertyName, parserContext.getDelegate().parsePropertySubElement(refElements.get(0), null)); } handleAdapterMethodAttribute(propertyName, adapterClassName, propertyValues, element); } /** * Handle the adapter-method attribute by using an * AbstractMethodInvokingDelegator */ private void handleAdapterMethodAttribute(String propertyName, String adapterClassName, MutablePropertyValues stepPvs, Element element) { String adapterMethodName = element.getAttribute("adapter-method"); if (StringUtils.hasText(adapterMethodName)) { // // Create an adapter // AbstractBeanDefinition adapterDef = new GenericBeanDefinition(); adapterDef.setBeanClassName(adapterClassName); MutablePropertyValues adapterPvs = adapterDef.getPropertyValues(); adapterPvs.addPropertyValue("targetMethod", adapterMethodName); // Inject the bean into the adapter adapterPvs.addPropertyValue("targetObject", stepPvs.getPropertyValue(propertyName).getValue()); // // Inject the adapter into the step // stepPvs.addPropertyValue(propertyName, adapterDef); } } private void handleRetryListenersElement(Element element, MutablePropertyValues propertyValues, ParserContext parserContext, BeanDefinition enclosing) { Element listenersElement = DomUtils.getChildElementByTagName(element, "retry-listeners"); if (listenersElement != null) { CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(listenersElement.getTagName(), parserContext.extractSource(element)); parserContext.pushContainingComponent(compositeDef); ManagedList<BeanMetadataElement> retryListenerBeans = new ManagedList<BeanMetadataElement>(); retryListenerBeans.setMergeEnabled(listenersElement.hasAttribute(MERGE_ATTR) && Boolean.valueOf(listenersElement.getAttribute(MERGE_ATTR))); handleRetryListenerElements(parserContext, listenersElement, retryListenerBeans, enclosing); propertyValues.addPropertyValue("retryListeners", retryListenerBeans); parserContext.popAndRegisterContainingComponent(); } } private void handleRetryListenerElements(ParserContext parserContext, Element element, ManagedList<BeanMetadataElement> beans, BeanDefinition enclosing) { List<Element> listenerElements = DomUtils.getChildElementsByTagName(element, "listener"); if (listenerElements != null) { for (Element listenerElement : listenerElements) { beans.add(AbstractListenerParser.parseListenerElement(listenerElement, parserContext, enclosing)); } } } private void handleStreamsElement(Element element, MutablePropertyValues propertyValues, ParserContext parserContext) { Element streamsElement = DomUtils.getChildElementByTagName(element, "streams"); if (streamsElement != null) { ManagedList<RuntimeBeanReference> streamBeans = new ManagedList<RuntimeBeanReference>(); streamBeans.setMergeEnabled(streamsElement.hasAttribute(MERGE_ATTR) && Boolean.valueOf(streamsElement.getAttribute(MERGE_ATTR))); List<Element> streamElements = DomUtils.getChildElementsByTagName(streamsElement, "stream"); if (streamElements != null) { for (Element streamElement : streamElements) { String streamRef = streamElement.getAttribute(REF_ATTR); if (StringUtils.hasText(streamRef)) { streamBeans.add(new RuntimeBeanReference(streamRef)); } else { parserContext.getReaderContext().error( REF_ATTR + " not specified for <" + streamElement.getTagName() + "> element", element); } } } propertyValues.addPropertyValue("streams", streamBeans); } } }